/*
 * FTNANDC024 driver developed by Mychaela Falconia at CarrierComm, Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <linux/io.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/ioport.h>
#include <linux/mtd/mtd.h>
#include <linux/wait.h>
#include <linux/errno.h>

#include "ftnandc024_priv.h"
#include "ftnandc024_regs.h"

static int pio_write_sectors(struct mtd_info *mtd, loff_t to, size_t len,
			     const u_char *buf, bool oob)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	uint32_t command;

	/* write the data into the SRAM */
	priv->page_in_buffer = -1;
	memcpy(priv->data_sram, buf, len);

	/* clear controller state before issuing command */
	writel(0, priv->io_base + BMC_USER_ADJUST(0));
	if (priv->smallpage_cmds && !oob)
		memset(priv->io_base + SPARE_SRAM, 0xFF, 32);

	/* issue Write Sectors command */
	writel(to >> mtd->writesize_shift, priv->io_base + CMDQUEUE1(0));
	writel(CMD_COLUMN((to & mtd->writesize_mask) >> priv->sector_shift) |
	       CMD_COUNT(len >> priv->sector_shift),
		priv->io_base + CMDQUEUE3(0));
	if (priv->smallpage_cmds)
		command = CMD_INDEX(SMALL_FIXFLOW_PAGEWRITE) |
				CMD_SPARE_NUM(mtd->oobsize);
	else if (oob)
		command = CMD_INDEX(LARGE_FIXFLOW_PAGEWRITE_OOB) |
				CMD_SPARE_NUM(mtd->oobsize);
	else
		command = CMD_INDEX(LARGE_FIXFLOW_PAGEWRITE);
	return ftnandc024_execute_command(priv, command | CMD_USER_MODE);
}

static int write_under_mutex(struct mtd_info *mtd, loff_t to, size_t len,
			     size_t *retlen, const u_char *buf)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	size_t startlen = len;
	unsigned chunk;
	int rc = 0;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc) {
			*retlen = 0;
			return rc;
		}
	}

	/* handle non-buffer-aligned start first */
	if (to & (SRAM_BUF_SIZE - 1)) {
		chunk = SRAM_BUF_SIZE - (to & (SRAM_BUF_SIZE - 1));
		if (chunk > len)
			chunk = len;
		rc = pio_write_sectors(mtd, to, chunk, buf, false);
		if (unlikely(rc)) {
			*retlen = 0;
			return rc;
		}
		buf += chunk;
		to += chunk;
		len -= chunk;
	}

	/* write full buffers and/or trailing partial buffer */
	while (len) {
		chunk = SRAM_BUF_SIZE;
		if (chunk > len)
			chunk = len;
		rc = pio_write_sectors(mtd, to, chunk, buf, false);
		if (unlikely(rc))
			break;
		buf += chunk;
		to += chunk;
		len -= chunk;
	}

	*retlen = startlen - len;
	return rc;
}

int ftnandc024_mtd_write_method(struct mtd_info *mtd, loff_t to, size_t len,
				size_t *retlen, const u_char *buf)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int ret;

	/* Do not allow writes past end of device */
	if ((to + len) > mtd->size)
		return -EINVAL;
	if (!len)
		return 0;

	/* Enforce page or subpage alignment */
	if ((to & priv->subpage_mask) || (len & priv->subpage_mask))
		return -EINVAL;

	mutex_lock(&priv->mutex);
	ret = write_under_mutex(mtd, to, len, retlen, buf);
	mutex_unlock(&priv->mutex);
	return ret;
}

static int writeops_data_and_oob(struct mtd_info *mtd, loff_t to,
				 struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	size_t writelen = ops->len;
	size_t oobwritelen = ops->ooblen;
	const uint8_t *buf = ops->datbuf;
	const uint8_t *oob = ops->oobbuf;
	unsigned chunk;
	int rc = 0;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc)
			return rc;
	}

	while (writelen) {
		/* handle OOB */
		memset(priv->io_base + SPARE_SRAM, 0xFF, mtd->oobsize);
		if (oob && oobwritelen) {
			chunk = mtd->oobsize;
			if (chunk > oobwritelen)
				chunk = oobwritelen;
			memcpy(priv->io_base + SPARE_SRAM + ops->ooboffs, oob,
				chunk);
			oob += chunk;
			oobwritelen -= chunk;
		}
		rc = pio_write_sectors(mtd, to, mtd->writesize, buf, true);
		if (unlikely(rc))
			break;
		buf += mtd->writesize;
		to += mtd->writesize;
		writelen -= mtd->writesize;
	}

	ops->retlen = ops->len - writelen;
	if (oob)
		ops->oobretlen = ops->ooblen - oobwritelen;
	return rc;
}

static int write_onepage_oob(struct mtd_info *mtd, unsigned row_addr)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	uint32_t command;

	/* issue command to write the OOB area of the requested page */
	writel(row_addr, priv->io_base + CMDQUEUE1(0));
	writel(CMD_COUNT(1), priv->io_base + CMDQUEUE3(0));
	if (priv->smallpage_cmds)
		command = CMD_INDEX(SMALL_FIXFLOW_WRITEOOB);
	else
		command = CMD_INDEX(LARGE_FIXFLOW_WRITEOOB);
	command |= CMD_SPARE_NUM(mtd->oobsize);
	return ftnandc024_execute_command(priv, command);
}

static int writeops_oob_only(struct mtd_info *mtd, loff_t to,
			     struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	unsigned row_addr = to >> mtd->writesize_shift;
	size_t writelen = ops->ooblen;
	const uint8_t *oob = ops->oobbuf;
	unsigned chunk;
	int rc = 0;

	if (unlikely(priv->reset_needed)) {
		rc = ftnandc024_reset_nand_chip(priv);
		if (rc)
			return rc;
	}

	while (writelen) {
		memset(priv->io_base + SPARE_SRAM, 0xFF, mtd->oobsize);
		chunk = mtd->oobsize;
		if (chunk > writelen)
			chunk = writelen;
		memcpy(priv->io_base + SPARE_SRAM + ops->ooboffs, oob, chunk);
		rc = write_onepage_oob(mtd, row_addr);
		if (unlikely(rc))
			break;
		oob += chunk;
		writelen -= chunk;
		row_addr++;
	}

	ops->oobretlen = ops->ooblen - writelen;
	return rc;
}

int ftnandc024_mtd_writeoob_method(struct mtd_info *mtd, loff_t to,
				   struct mtd_oob_ops *ops)
{
	struct ftnandc024_data *priv = to_ftnandc024_data(mtd);
	int rc;

	ops->retlen = 0;
	ops->oobretlen = 0;

	/* sanity checks */
	if (ops->mode != MTD_OPS_PLACE_OOB && ops->mode != MTD_OPS_AUTO_OOB)
		return -EOPNOTSUPP;
	/* don't allow writes past the end of the device */
	/* enforce page alignment */
	if (unlikely(to >= mtd->size))
		return -EINVAL;
	if (unlikely(to & mtd->writesize_mask))
		return -EINVAL;
	if (ops->datbuf) {
		if (ops->len > (mtd->size - to))
			return -EINVAL;
		if (unlikely(ops->len & mtd->writesize_mask))
			return -EINVAL;
	}
	if (unlikely(ops->ooboffs >= mtd->oobsize))
		return -EINVAL;
	if (ops->ooboffs && ops->ooblen > (mtd->oobsize - ops->ooboffs))
		return -EINVAL;
	if (unlikely(ops->ooblen > ((mtd->size >> mtd->writesize_shift) -
				    (to >> mtd->writesize_shift)) *
				   mtd->oobsize))
		return -EINVAL;

	mutex_lock(&priv->mutex);
	if (ops->datbuf)
		rc = writeops_data_and_oob(mtd, to, ops);
	else
		rc = writeops_oob_only(mtd, to, ops);
	mutex_unlock(&priv->mutex);
	return rc;
}
